Skip to content

fix(plugin-security): project env-scope permission-set edits onto sys_permission_set (#2857)#2867

Merged
os-zhuang merged 1 commit into
mainfrom
fix/perm-set-data-projection
Jul 13, 2026
Merged

fix(plugin-security): project env-scope permission-set edits onto sys_permission_set (#2857)#2867
os-zhuang merged 1 commit into
mainfrom
fix/perm-set-data-projection

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Closes the split-brain where an environment-scope permission-set edit updates the metadata overlay but not the queryable sys_permission_set record. Fixes #2857.

Why

A sys_permission_set has two representations:

  • Metadata — what client.save('permission', …) / client.layered read-write (authoritative; enforcement + ADR-0090 explain read it).
  • Data record/api/v1/data/sys_permission_set, snake_case JSON-string columns, what the admin/Setup surface reads.

The camelCase→snake projection (toRowFields / upsertPackagePermissionSet) runs only at boot and on publish (the package door), and the publish path refuses env-authored rows. So an env-scope saveMetaItem('permission', …) persists the sys_metadata overlay (the layered read reflects it) but never touches the record → the admin surface goes stale. Enforcement stays correct (it reads the metadata), so this is display-freshness, but it breaks the loop for the objectui pure-model work (objectstack-ai/objectui#2403: Studio designs → Setup shows read-only).

How

Adds the environment door, mirroring the existing authored-translation-sync mutation-listener pattern:

  • subscribeEnvPermissionProjection(protocol, ql, logger) — hooks the protocol's post-persistence onMetadataMutation choke point (wired once in SecurityPlugin.runBootstrap, guarded so runBootstrap's re-run doesn't double-subscribe).
  • projectEnvPermissionOnMutation — on an active (non-draft) permission event, re-reads the fresh effective body via getMetaItemLayered (the boot-cached MetadataManager registry would hand back a stale declared body for a seeded set), tolerating both the layered envelope ({effective|code}) and a direct body.
  • upsertEnvPermissionSet — projects the six facets (objectsobject_permissions, …, adminScopeadmin_scope). Ownership is decided by the record's managed_by, never the body: the layered read stamps _packageId provenance on env-authored sets too, so only the record can distinguish the doors. Env-authored rows (platform/user/absent) are projected; a package-owned record's baseline is left to boot re-seed / publish.

Storage columns unchanged.

Tested

  • Unit — 21 tests (bootstrap-declared-permissions.test.ts): projection for all six facets; env upsert vs package-record refusal; the _packageId-provenance regression (record decides, not body); the direct-body-shape regression (getMetaItemLayered can return the body directly — a ?? null fallback would silently drop it); and the mutation wiring via a mock protocol (envelope + direct shapes, draft/non-permission skip, no-onMetadataMutation → no-op).
  • No regression — 153 plugin-security tests pass.

⚠️ Verification honesty

I could not get a clean live end-to-end (start backend → env save → assert the record refreshes) in my local isolated worktree backend: require.resolve('@objectstack/plugin-security') fails from the example app, and the dev server loaded a stale plugin-security via pnpm/worktree resolution rather than my rebuilt dist, so the running backend never had this code (a marker file placed at the subscriber's first line never appeared across 7 rebuild/restart attempts). This is a worktree/pnpm-resolution artifact of my local setup, not a code issue — the projection + wiring logic is deterministically unit-tested above, and the wiring mirrors the proven authored-translation-sync listener. Please confirm the live projection under CI / a clean install. (The two bugs the unit tests caught — the over-eager _packageId guard and the dropped direct-body — were found during this debugging, so it was productive.)

Refs

🤖 Generated with Claude Code

…_permission_set (#2857)

An environment-scope save('permission', ...) writes only the sys_metadata
overlay; the metadata->sys_permission_set projection ran only at boot / on
publish (and publish refuses env-authored rows), so the queryable record the
admin/Setup surface reads went stale while the layered read showed the edit.

Adds the environment door: subscribeEnvPermissionProjection hooks the protocol's
post-persistence onMetadataMutation choke point; on an active permission save it
re-reads the fresh effective body via the layered read and upsertEnvPermissionSet
projects the six facet columns onto the record. Ownership is decided by the
RECORD's managed_by (env-authored rows only; package records keep their declared
baseline). Mirrors authored-translation-sync's mutation-listener pattern.

Tested: 21 unit tests (projection for all six facets, env/package guards, the
_packageId-provenance + direct-body-shape regressions, and the mutation wiring via
a mock protocol); no regression (153 plugin-security tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 13, 2026 2:41am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security.

11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-security)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sys_permission_set: env-scope permission metadata saves don't project onto the queryable data record

1 participant